function Texture(gl) {
  this.gl = gl;
  this.texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, this.texture);

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}

Texture.prototype.bind = function (n, program, name) {
  this.gl.activeTexture([this.gl.TEXTURE0, this.gl.TEXTURE1, this.gl.TEXTURE2][n]);
  this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
  this.gl.uniform1i(this.gl.getUniformLocation(program, name), n);
};

Texture.prototype.fill = function (width, height, data) {
  this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
  this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.LUMINANCE, width, height, 0, this.gl.LUMINANCE, this.gl.UNSIGNED_BYTE, data);
};

function WebGLPlayer(canvas, options) {
  this.canvas = canvas;
  this.gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
  this.initGL(options);
}

WebGLPlayer.prototype.initGL = function (options) {
  if (!this.gl) {
    console.log("[ER] WebGL not supported.");
    return;
  }

  this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT, 1);
  let program = this.gl.createProgram();
  let vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);

  this.gl.shaderSource(vertexShader, [
    "attribute highp vec4 aVertexPosition;",
    "attribute vec2 aTextureCoord;",
    "varying highp vec2 vTextureCoord;",
    "void main(void) {",
    " gl_Position = aVertexPosition;",
    " vTextureCoord = aTextureCoord;",
    "}"
  ].join("\n"));

  this.gl.compileShader(vertexShader);
  let fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
  this.gl.shaderSource(fragmentShader, [
    "precision highp float;",
    "varying lowp vec2 vTextureCoord;",
    "uniform sampler2D YTexture;",
    "uniform sampler2D UTexture;",
    "uniform sampler2D VTexture;",
    "const mat4 YUV2RGB = mat4",
    "(",
    " 1.1643828125, 0, 1.59602734375, -.87078515625,",
    " 1.1643828125, -.39176171875, -.81296875, .52959375,",
    " 1.1643828125, 2.017234375, 0, -1.081390625,",
    " 0, 0, 0, 1",
    ");",
    "void main(void) {",
    " gl_FragColor = vec4( texture2D(YTexture, vTextureCoord).x, texture2D(UTexture, vTextureCoord).x, texture2D(VTexture, vTextureCoord).x, 1) * YUV2RGB;",
    "}"
  ].join("\n"));

  this.gl.compileShader(fragmentShader);
  this.gl.attachShader(program, vertexShader);
  this.gl.attachShader(program, fragmentShader);
  this.gl.linkProgram(program);
  this.gl.useProgram(program);
  if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
    console.log("[ER] Shader link failed.");
  }
  let vertexPositionAttribute = this.gl.getAttribLocation(program, "aVertexPosition");
  this.gl.enableVertexAttribArray(vertexPositionAttribute);
  let textureCoordAttribute = this.gl.getAttribLocation(program, "aTextureCoord");
  this.gl.enableVertexAttribArray(textureCoordAttribute);

  let verticesBuffer = this.gl.createBuffer();
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, verticesBuffer);
  this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0]), this.gl.STATIC_DRAW);
  this.gl.vertexAttribPointer(vertexPositionAttribute, 3, this.gl.FLOAT, false, 0, 0);
  let texCoordBuffer = this.gl.createBuffer();
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, texCoordBuffer);
  this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]), this.gl.STATIC_DRAW);
  this.gl.vertexAttribPointer(textureCoordAttribute, 2, this.gl.FLOAT, false, 0, 0);

  this.gl.y = new Texture(this.gl);
  this.gl.u = new Texture(this.gl);
  this.gl.v = new Texture(this.gl);
  this.gl.y.bind(0, program, "YTexture");
  this.gl.u.bind(1, program, "UTexture");
  this.gl.v.bind(2, program, "VTexture");
};

WebGLPlayer.prototype.renderFrame = function (videoFrame, width, height, uOffset, vOffset) {
  if (!this.gl) {
    console.log("[ER] Render frame failed due to WebGL not supported.");
    return;
  }

  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
  this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);

  this.gl.y.fill(width, height, videoFrame.subarray(0, uOffset));
  this.gl.u.fill(width >> 1, height >> 1, videoFrame.subarray(uOffset, uOffset + vOffset));
  this.gl.v.fill(width >> 1, height >> 1, videoFrame.subarray(uOffset + vOffset, videoFrame.length));
  delete (videoFrame);
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
};

WebGLPlayer.prototype.fullscreen = function () {
  let canvas = this.canvas;
  if (canvas.RequestFullScreen) {
    canvas.RequestFullScreen();
  } else if (canvas.webkitRequestFullScreen) {
    canvas.webkitRequestFullScreen();
  } else if (canvas.mozRequestFullScreen) {
    canvas.mozRequestFullScreen();
  } else if (canvas.msRequestFullscreen) {
    canvas.msRequestFullscreen();
  } else {
    alert("This browser doesn't supporter fullscreen");
  }
};

WebGLPlayer.prototype.exitfullscreen = function () {
  if (document.exitFullscreen) {
    document.exitFullscreen();
  } else if (document.webkitExitFullscreen) {
    document.webkitExitFullscreen();
  } else if (document.mozCancelFullScreen) {
    document.mozCancelFullScreen();
  } else if (document.msExitFullscreen) {
    document.msExitFullscreen();
  } else {
    alert("Exit fullscreen doesn't work");
  }
};
